package game.maze;

import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import shared.Tiles;

public class MazeGenerator {
   /**
    * Road width (the with of two tiles wall+floor)
    */
   public static final int RWIDTH = 2;
   /**
    * Tilemap
    */
   private byte[][] map;
   private Random rnd;
   
   public MazeGenerator(int width, int height) {
      map = new byte[width][height];
      rnd = new Random();
   }
   
   /**
    * @param seed    seed will always return same map
    */
   public MazeGenerator(int width, int height, long seed) {
      map = new byte[width][height];
      rnd = new Random(seed);
   }
   
   /**
    * Returns a random UNIQUE maze part
    */
   public byte[][] getRandomMaze(int players) {
      if (!(players == 1 || players == 2 || players == 4)) return null;
      createMaze();
      if (players == 2 || players == 4) {
         map = getQuadMap(map);
      }
      map = setWallBorders(map);
      return map;
   }
   
   /**
    * Builds a 2x2 full map out of the UNIQUE parts
    * Every part of the map is rotated/flipped
    */
   private byte[][] getQuadMap(byte[][] map) {
      printMaze();
      byte[][] newMap = new byte[getWidth() * 2 - 1][getHeight() * 2 - 1];
      for (int x = 0; x < getWidth(); x++) {
         for (int y = 0; y < getHeight(); y++) {
            newMap[x][y] = map[x][y];
            byte newTile = map[getWidth() - 1 - x][y];
            if (newTile == Tiles.PORTAL_1) newTile = Tiles.PORTAL_2;
            else if (newTile == Tiles.PORTAL_2) newTile = Tiles.PORTAL_1;
            newMap[getWidth() + x - 1][y] = newTile;
         }
      }
      
      for (int x = 0; x < getWidth(); x++) {
         for (int y = 0; y < getHeight(); y++) {
            byte newTile = map[getWidth() - 1 - x][y];
            if (newTile == Tiles.PORTAL_1) newTile = Tiles.PORTAL_2;
            else if (newTile == Tiles.PORTAL_2) newTile = Tiles.PORTAL_1;
            newMap[getWidth() - x - 1][getHeight()*2 - 2 - y] = newTile;
            newMap[getWidth() + x - 1][getHeight()*2 - 2 - y] = newTile;
         }
      }
      return newMap;
   }
   
   private byte[][] setWallBorders(byte[][] map) {
      byte[][] newMap = new byte[getWidth() + 2][getHeight() + 2];
      int x, y;
      for (x = 0; x < getWidth() + 2; x++) {
         newMap[x][0] = Tiles.WALL;
         newMap[x][getHeight() + 1] = Tiles.WALL;
      }
      for (y = 1; y < getHeight(); y++) {
         newMap[0][y] = Tiles.WALL;
         newMap[getWidth() + 1][y] = Tiles.WALL;
      }
      for (x = 0; x < getWidth(); x++) {
         for (y = 0; y < getHeight(); y++) {
            newMap[x+1][y+1] = map[x][y];
         }
      }
      return newMap;
   }
   
   public int getWidth() {
      return map.length;
   }
   
   public int getHeight() {
      return map[0].length;
   }
   
   public byte getTile(int x, int y) {
      try {
         return map[x][y];
      } catch (IndexOutOfBoundsException e) {
         return Tiles.OUT_OF_BOUNDS;
      }
   }
   
   /**
    * Sets a tile to the given type
    */
   private void setTile(byte tile, int x, int y) {
      map[x][y] = tile;
   }
   
   /**
    * Sets a tile to the given type
    */
   private void setTile(byte tile, Point room) {
      map[room.x][room.y] = tile;
   }
   
   /**
    * Sets an area on the map to the given type
    * @param tile    the type we with to place
    * @param p1      first point
    * @param p2      second point
    */
   private void setArea(byte tile, Point p1, Point p2) {
      int x1, x2, y1, y2;
      if (p1.x < p2.x) {
         x1 = p1.x;
         x2 = p2.x;
      } else {
         x2 = p1.x;
         x1 = p2.x;
      }
      if (p1.y < p2.y) {
         y1 = p1.y;
         y2 = p2.y;
      } else {
         y2 = p1.y;
         y1 = p2.y;
      }
      for (int x = x1; x <= x2; x++) {
         for (int y = y1; y <= y2; y++) {
            setTile(tile, x, y);
         }
      }
   }
   
   /**
    * Creates the maze
    */
   private void createMaze() {
      int startX = getWidth() / RWIDTH;
      int startY = getHeight() / RWIDTH;
      startX *= RWIDTH;
      startY *= RWIDTH;
      Point room = new Point(startX, startY);
      Point prevRoom = new Point(startX, startY);
      
      List<Point> possibleRoads = new ArrayList<Point>();
      possibleRoads.add(room);
      
      while (getEmptySlot() != null) {
         prevRoom = possibleRoads.remove(rnd.nextInt(possibleRoads.size()));
         while ((room = getRndRoom(Tiles.WALL, RWIDTH, prevRoom)) != null) {
            possibleRoads.add(room);
            setArea(Tiles.FLOOR, prevRoom, room);
            prevRoom = room;
         }
      }
      setTile(Tiles.EXIT, startX, startY);
   }
   
   /**
    * Finds a random wall on the map.
    */
   private Point getRandomWall() {
      //this approach is faster than making a list of all and choosing from them
      int x = rnd.nextInt(getWidth());
      int y = rnd.nextInt(getHeight());
      if (getTile(x, y) == Tiles.WALL) {
         return new Point(x, y);
      } else {
         return getRandomWall();
      }
   }
   
   /**
    * Finds possible 1 cell big island positions
    * @return  first possible island position
    */
   private Point getEmptySlot() {
      for (int x = 0; x < getWidth(); x+=RWIDTH) {
         for (int y = 0; y < getHeight(); y += RWIDTH) {
            if (getTile(x, y) == Tiles.WALL) {
               return new Point(x, y);
            }
         }
      }
      return null;
   }
   
   /**
    * Finds a random bridge between given point and tiles around it
    * @param tile    the type of tile we want to connect with
    * @param room    one side of the bridge
    * @return  random possible bridge or null if none exist
    */
   private Point getRndRoom(byte tile, int range, Point room) {
      List<Point> rooms = new ArrayList<Point>();
      if (getTile(room.x + range, room.y) == tile)rooms.add(new Point(room.x + range, room.y));
      if (getTile(room.x - range, room.y) == tile)rooms.add(new Point(room.x - range, room.y));
      if (getTile(room.x, room.y + range) == tile)rooms.add(new Point(room.x, room.y + range));
      if (getTile(room.x, room.y - range) == tile)rooms.add(new Point(room.x, room.y - range));
      if (rooms.size() > 0) {
         return rooms.get(rnd.nextInt(rooms.size()));
      }
      return null;
   }
   
   /**
    * Print out the maze
    */
   public void printMaze() {
      for (int y = 0; y < getHeight(); y++) {
         for (int x = 0; x < getWidth(); x++) {
            if (getTile(x, y) == Tiles.WALL) {
               System.out.print("@@");
            } else if (getTile(x, y) == Tiles.PORTAL_1) {
               System.out.print("/A");
            } else if (getTile(x, y) == Tiles.PORTAL_2) {  
               System.out.print("/B");
            } else if (getTile(x, y) == Tiles.FLOOR){
               System.out.print("..");
            } else if (getTile(x, y) == Tiles.EXIT) {
               System.out.print("EX");
            } else {
               System.out.print(getTile(x, y));
            }
         }
         System.out.print("\n");
      }
   }
}
